<template>
  <div class="serial-panel">
    <div class="toolbar">
      <el-select v-model="form.port" size="small" placeholder="选择端口" class="w-180" filterable>
        <el-option v-for="p in ports" :key="p.path" :label="p.path" :value="p.path" />
      </el-select>
      <el-select v-model="form.baudRate" size="small" placeholder="波特率" class="w-140">
        <el-option
          v-for="b in mergedBaudRates"
          :key="b"
          :label="isFavBaud(b) ? `${b} ★` : String(b)"
          :value="b"
        />
      </el-select>
      <el-button size="small" @click="toggleFavBaud" :type="isFavBaud(form.baudRate) ? 'warning' : 'default'">{{ isFavBaud(form.baudRate) ? '取消收藏' : '收藏速率' }}</el-button>
      <el-select v-model="form.dataBits" size="small" placeholder="数据位" class="w-120">
        <el-option :label="7" :value="7" />
        <el-option :label="8" :value="8" />
      </el-select>
      <el-select v-model="form.stopBits" size="small" placeholder="停止位" class="w-120">
        <el-option :label="1" :value="1" />
        <el-option :label="2" :value="2" />
      </el-select>
      <el-select v-model="form.parity" size="small" placeholder="校验" class="w-120">
        <el-option label="none" value="none" />
        <el-option label="even" value="even" />
        <el-option label="odd" value="odd" />
      </el-select>
      <el-button size="small" type="primary" @click="handleRefresh">刷新</el-button>
      <el-button size="small" type="success" :disabled="connected" @click="connect">连接</el-button>
      <el-button size="small" type="danger" :disabled="!connected" @click="disconnect">断开</el-button>
      <el-tag :type="connected ? 'success' : 'info'" class="status-tag">{{ connected ? '已连接' : '未连接' }}</el-tag>
      <div class="flex-spacer" />
      <el-switch v-model="isDark" size="small" active-text="暗色" @change="applyTheme" />
      <el-radio-group v-model="mode" size="small" @change="onModeChange">
        <el-radio-button label="webserial">Web Serial</el-radio-button>
        <el-radio-button label="bridge">本地网桥</el-radio-button>
      </el-radio-group>
    </div>

    <div class="cards">
      <el-card shadow="hover">
        <template #header>
          <div class="card-title">发送</div>
        </template>
        <div class="send-panel">
          <div class="send-history">
            <el-select v-model="sendText" size="small" filterable allow-create default-first-option placeholder="历史/输入..." class="w-180">
              <el-option v-for="h in history" :key="h" :label="h" :value="h" />
            </el-select>
          </div>
          <el-radio-group v-model="sendMode" size="small">
            <el-radio-button label="ascii">ASCII</el-radio-button>
            <el-radio-button label="hex">HEX</el-radio-button>
          </el-radio-group>
          <el-input
            v-model="sendText"
            :rows="7"
            type="textarea"
            placeholder="输入要发送的数据"
            class="mt-8"
          />
          <div class="send-actions">
            <el-checkbox v-model="appendNewline">附加换行(\n)</el-checkbox>
            <el-button size="small" type="primary" :disabled="!connected" @click="send">发送</el-button>
          </div>
          <div class="quick-cmds">
            <el-input v-model="newQuick" size="small" placeholder="添加快捷指令" class="w-180" @keyup.enter="addQuick" />
            <el-button size="small" @click="addQuick">添加</el-button>
            <div class="quick-wrap">
              <el-tag
                v-for="q in quickCommands"
                :key="q"
                class="quick-tag"
                closable
                @close="removeQuick(q)"
                @click="useQuick(q)"
              >{{ q }}</el-tag>
            </div>
          </div>
        </div>
      </el-card>

      <el-card shadow="hover">
        <template #header>
          <div class="card-title">接收</div>
        </template>
        <div class="recv-toolbar">
          <el-radio-group v-model="viewMode" size="small">
            <el-radio-button label="ascii">ASCII</el-radio-button>
            <el-radio-button label="hex">HEX</el-radio-button>
          </el-radio-group>
          <div>
            <el-checkbox v-model="autoScroll" style="margin-right:8px;">自动滚动</el-checkbox>
            <el-button size="small" @click="clearRecv">清空</el-button>
          </div>
        </div>
        <pre class="recv-box" ref="recvBox">{{ displayLogs }}</pre>
      </el-card>
    </div>
  </div>
  
</template>

<script setup>
import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { ElMessage } from 'element-plus'

// 本组件直接使用 Web Serial API（或本地网桥）
let serial = null
// 模式：'webserial' | 'bridge'（本地 Node 网桥）。默认 webserial
const mode = ref(localStorage.getItem('serialMode') || 'webserial')
const preferBackendBridge = computed(() => mode.value === 'bridge')

function initWebSerial() {
  if (!('serial' in navigator)) return
  const subscribers = {
    data: [], // (data: string) => void
    error: [], // (err: string) => void
    status: [] // ({ connected: boolean }) => void
  }
  const idToPort = new Map()
  let currentPort = null
  let reader = null

  const emit = (type, payload) => {
    for (const cb of subscribers[type]) {
      try { cb(payload) } catch (e) { void 0 }
    }
  }

  const bytesToAscii = (bytes) => {
    try { return new TextDecoder().decode(bytes) } catch (e) { return '' }
  }

  const hexToBytes = (hex) => {
    const cleaned = String(hex).replace(/[^0-9a-fA-F]/g, '')
    const out = new Uint8Array(Math.floor(cleaned.length / 2))
    for (let i = 0; i < out.length; i++) {
      out[i] = parseInt(cleaned.substr(i * 2, 2), 16)
    }
    return out
  }

  const list = async () => {
    idToPort.clear()
    const ports = await navigator.serial.getPorts().catch(() => [])
    const results = []
    ports.forEach((p, idx) => {
      const info = p.getInfo?.() || {}
      let path
      if (info.usbVendorId && info.usbProductId) {
        const vid = info.usbVendorId.toString(16).padStart(4, '0').toUpperCase()
        const pid = info.usbProductId.toString(16).padStart(4, '0').toUpperCase()
        path = `USB VID:${vid} PID:${pid} #${idx}`
      } else {
        path = `Serial Port #${idx}`
      }
      idToPort.set(path, p)
      results.push({ path })
    })
    return results
  }

  

  const open = async (opts) => {
    const port = idToPort.get(opts.port)
    if (!port) throw new Error('未选择或未授权串口')
    await port.open({
      baudRate: Number(opts.baudRate) || 115200,
      dataBits: Number(opts.dataBits) || 8,
      stopBits: Number(opts.stopBits) || 1,
      parity: String(opts.parity || 'none')
    })
    currentPort = port
    emit('status', { connected: true })

    // 启动读循环
    if (port.readable) {
      reader = port.readable.getReader()
      ;(async () => {
        try {
          for (;;) {
            const { value, done } = await reader.read()
            if (done) break
            if (value && value.length) emit('data', bytesToAscii(value))
          }
        } catch (e) {
          emit('error', String(e))
        } finally {
          try { reader.releaseLock() } catch (e) { void 0 }
          reader = null
        }
      })()
    }
  }

  const close = async () => {
    if (reader) {
      try { await reader.cancel() } catch (e) { void 0 }
    }
    if (currentPort) {
      try { await currentPort.close() } catch (e) { void 0 }
    }
    currentPort = null
    emit('status', { connected: false })
  }

  const write = async (payload) => {
    if (!currentPort || !currentPort.writable) throw new Error('未连接串口')
    const writer = currentPort.writable.getWriter()
    try {
      if (payload.mode === 'hex') {
        await writer.write(hexToBytes(payload.data))
      } else {
        await writer.write(new TextEncoder().encode(String(payload.data)))
      }
    } finally {
      try { writer.releaseLock() } catch (e) { void 0 }
    }
  }

  serial = {
    list,
    open,
    close,
    write,
    onData: (cb) => subscribers.data.push(cb),
    onError: (cb) => subscribers.error.push(cb),
    onStatus: (cb) => subscribers.status.push(cb)
  }

  serial.onData(onData)
  serial.onError(onError)
  serial.onStatus(onStatus)
}

// 连接策略：优先本机桥接（每台客户端访问各自的串口），失败再退回服务器主机
function resolveBridgeCandidates() {
  const port = (window.SERIAL_BRIDGE_PORT || 3323)
  // 强制仅连接本机网桥，确保每台设备只读取自身串口
  return [`ws://localhost:${port}`]
}

// 浏览器环境适配：如果没有 Electron 的 window.serial，但支持 Web Serial API，则提供一个兼容的 serial 对象
if (!preferBackendBridge.value && ('serial' in navigator)) {
  const subscribers = {
    data: [], // (data: string) => void
    error: [], // (err: string) => void
    status: [] // ({ connected: boolean }) => void
  }
  const idToPort = new Map()
  let currentPort = null
  let reader = null

  const emit = (type, payload) => {
    for (const cb of subscribers[type]) {
      try { cb(payload) } catch (e) { void 0 }
    }
  }

  const bytesToAscii = (bytes) => {
    try { return new TextDecoder().decode(bytes) } catch (e) { return '' }
  }

  const hexToBytes = (hex) => {
    const cleaned = String(hex).replace(/[^0-9a-fA-F]/g, '')
    const out = new Uint8Array(Math.floor(cleaned.length / 2))
    for (let i = 0; i < out.length; i++) {
      out[i] = parseInt(cleaned.substr(i * 2, 2), 16)
    }
    return out
  }

  const list = async () => {
    idToPort.clear()
    const ports = await navigator.serial.getPorts().catch(() => [])
    const results = []
    ports.forEach((p, idx) => {
      const info = p.getInfo?.() || {}
      let path
      if (info.usbVendorId && info.usbProductId) {
        const vid = info.usbVendorId.toString(16).padStart(4, '0').toUpperCase()
        const pid = info.usbProductId.toString(16).padStart(4, '0').toUpperCase()
        path = `USB VID:${vid} PID:${pid} #${idx}`
      } else {
        path = `Serial Port #${idx}`
      }
      idToPort.set(path, p)
      results.push({ path })
    })
    return results
  }

  

  const open = async (opts) => {
    const port = idToPort.get(opts.port)
    if (!port) throw new Error('未选择或未授权串口')
    await port.open({
      baudRate: Number(opts.baudRate) || 115200,
      dataBits: Number(opts.dataBits) || 8,
      stopBits: Number(opts.stopBits) || 1,
      parity: String(opts.parity || 'none')
    })
    currentPort = port
    emit('status', { connected: true })

    // 启动读循环
    if (port.readable) {
      reader = port.readable.getReader()
      ;(async () => {
        try {
          for (;;) {
            const { value, done } = await reader.read()
            if (done) break
            if (value && value.length) emit('data', bytesToAscii(value))
          }
        } catch (e) {
          emit('error', String(e))
        } finally {
          try { reader.releaseLock() } catch (e) { void 0 }
          reader = null
        }
      })()
    }
  }

  const close = async () => {
    if (reader) {
      try { await reader.cancel() } catch (e) { void 0 }
    }
    if (currentPort) {
      try { await currentPort.close() } catch (e) { void 0 }
    }
    currentPort = null
    emit('status', { connected: false })
  }

  const write = async (payload) => {
    if (!currentPort || !currentPort.writable) throw new Error('未连接串口')
    const writer = currentPort.writable.getWriter()
    try {
      if (payload.mode === 'hex') {
        await writer.write(hexToBytes(payload.data))
      } else {
        await writer.write(new TextEncoder().encode(String(payload.data)))
      }
    } finally {
      try { writer.releaseLock() } catch (e) { void 0 }
    }
  }

  serial = {
    list,
    open,
    close,
    write,
    onData: (cb) => subscribers.data.push(cb),
    onError: (cb) => subscribers.error.push(cb),
    onStatus: (cb) => subscribers.status.push(cb)
  }
}

// 端口列表（例如 COM3、COM4 ...）
const ports = ref([])

// 是否已连接串口
const connected = ref(false)

// 接收区日志（按时间追加）。这里存原始字符串，展示时按 viewMode 决定 ASCII / HEX
const logs = ref([])

// 串口基本参数表单：串口号/波特率/数据位/停止位/校验位
const form = ref({
  port: '',
  baudRate: 115200,
  dataBits: 8,
  stopBits: 1,
  parity: 'none'
})

// 常见波特率（基础集）
const defaultBaudRates = [9600, 19200, 38400, 57600, 115200]

// 用户收藏的波特率（显示时置前）
const favBaudRates = ref([])

// 最终展示的波特率列表（去重：收藏优先 + 默认集）
const mergedBaudRates = computed(() => Array.from(new Set([
  ...favBaudRates.value,
  ...defaultBaudRates
])))

// 发送区文本
const sendText = ref('')
// 发送模式：'ascii' | 'hex'
const sendMode = ref('ascii')
// ASCII 发送时是否附加换行
const appendNewline = ref(false)
// 接收区显示模式：'ascii' | 'hex'
const viewMode = ref('ascii')
// 接收区自动滚动到底部
const autoScroll = ref(true)
// 接收区 <pre> DOM 引用，用来控制滚动
const recvBox = ref(null)
// 发送历史（最近最多 20 条，去重）
const history = ref([])
// 快捷指令列表（点击即填入发送框）
const quickCommands = ref(['AT', 'AT+GMR', 'ATI'])
// 新增快捷指令输入框
const newQuick = ref('')
// 是否暗色主题
const isDark = ref(false)
// 点击“刷新”时的统一入口：根据模式选择授权或直接刷新
async function handleRefresh() {
  if (!preferBackendBridge.value && typeof requestAndRefreshPorts === 'function') {
    await requestAndRefreshPorts()
  } else {
    await refreshPorts()
  }
}
// 浏览器模式下：请求授权并刷新端口列表
async function requestAndRefreshPorts() {
  if (!('serial' in navigator)) {
    ElMessage.warning('当前浏览器不支持 Web Serial API，请使用最新 Chrome/Edge')
    return
  }
  if (!window.isSecureContext) {
    ElMessage.warning('请通过 HTTPS 访问以启用本机串口（Web Serial 仅在安全上下文可用）')
    return
  }
  try {
    await navigator.serial.requestPort({ /* 可选: filters */ })
  } catch (e) {
    // 用户取消，无需提示为错误
  }
  const granted = await navigator.serial.getPorts().catch(() => [])
  const idToPort = new Map()
  const results = []
  let idx = 0
  for (const p of granted) {
    const info = p.getInfo?.() || {}
    const id = `USB-${info.usbVendorId || 'v'}:${info.usbProductId || 'p'}-${idx++}`
    idToPort.set(id, p)
    results.push({ path: id })
  }
  ports.value = results
  if (!form.value.port && ports.value.length) form.value.port = ports.value[0].path
}
// WebSocket 网桥
let ws = null
let reqSeq = 1
const pending = new Map()

/**
 * 计算属性：将 logs 转成最终要展示的字符串
 * - ASCII 模式：原样合并
 * - HEX 模式：按字节转十六进制（空格分隔）
 */
const displayLogs = computed(() => {
  const toHex = (text) => Array.from(new TextEncoder().encode(String(text)))
    .map(b => b.toString(16).padStart(2, '0')).join(' ')
  return logs.value.map(item => {
    if (viewMode.value === 'hex') return toHex(item)
    return String(item)
  }).join('\n')
})

// 刷新可用串口列表（仅读取已授权端口，或通过后端）
async function refreshPorts() {
  // 返回示例：[{ path: 'COM3' }, { path: 'COM4' }]
  if (serial && serial.list) {
    ports.value = await serial.list()
  } else if (ws && ws.readyState === WebSocket.OPEN) {
    const res = await rpc({ type: 'list' })
    ports.value = (res && res.ok && Array.isArray(res.payload)) ? res.payload : []
  } else {
    ports.value = []
  }
  if (!form.value.port && ports.value.length) form.value.port = ports.value[0].path
}

// 打开串口（参数来源：form）
async function connect() {
  if (serial && serial.open) {
    await serial.open({
      port: form.value.port,
      baudRate: form.value.baudRate,
      dataBits: form.value.dataBits,
      stopBits: form.value.stopBits,
      parity: form.value.parity
    })
    return
  }
  if (!ws || ws.readyState !== WebSocket.OPEN) {
    ElMessage.warning('后端串口网桥未连接')
    return
  }
  const res = await rpc({ type: 'open', options: {
    path: form.value.port,
    baudRate: form.value.baudRate,
    dataBits: form.value.dataBits,
    stopBits: form.value.stopBits,
    parity: form.value.parity
  }})
  if (!(res && res.ok)) ElMessage.error(String(res && res.payload || '打开失败'))
}

// 关闭串口
async function disconnect() {
  if (serial && serial.close) {
    await serial.close()
    return
  }
  if (!ws || ws.readyState !== WebSocket.OPEN) return
  await rpc({ type: 'close' })
}

// 发送数据
// - ASCII：可选附加 \n
// - HEX：传入十六进制字符串（原生层解析）
// - 发送成功后维护本地发送历史（最多 20 条）
async function send() {
  let data = sendText.value || ''
  if (appendNewline.value && sendMode.value === 'ascii') data += '\n'
  if (serial && serial.write) {
    await serial.write({ data, mode: sendMode.value })
  } else {
    if (!ws || ws.readyState !== WebSocket.OPEN) {
      ElMessage.warning('后端串口网桥未连接')
      return
    }
    const res = await rpc({ type: 'write', payload: { data, mode: sendMode.value } })
    if (!(res && res.ok)) ElMessage.error(String(res && res.payload || '发送失败'))
  }
  const entry = sendText.value.trim()
  if (entry && !history.value.includes(entry)) {
    history.value.unshift(entry)
    if (history.value.length > 20) history.value.pop()
  }
}

// 串口数据回调：将数据追加到接收日志，并根据需要自动滚动
function onData(data) {
  logs.value.push(data)
  nextTick(() => {
    if (autoScroll.value && recvBox.value) {
      recvBox.value.scrollTop = recvBox.value.scrollHeight
    }
  })
}

// 串口错误回调：简单记录错误文本
function onError(err) {
  logs.value.push(`[ERROR] ${err}`)
}

// 串口状态回调：更新「已连接」标记
function onStatus(status) {
  connected.value = !!status.connected
}

// 组件挂载后：拉取端口列表并注册监听
onMounted(() => {
  if (preferBackendBridge.value) {
    initBridge()
  } else {
    initWebSerial()
  }
  refreshPorts()
})

onBeforeUnmount(() => {
  // listeners remain for app lifetime; no explicit remove in this simple setup
})

// 清空接收区
function clearRecv() {
  logs.value = []
}

// 新增一条快捷指令（去空、去重）
function addQuick() {
  const v = newQuick.value.trim()
  if (!v) return
  if (!quickCommands.value.includes(v)) quickCommands.value.push(v)
  newQuick.value = ''
}

// 移除一条快捷指令
function removeQuick(q) {
  quickCommands.value = quickCommands.value.filter(i => i !== q)
}

// 使用某条快捷指令：填入发送框
function useQuick(q) {
  sendText.value = q
}

// 判断某波特率是否在收藏中
function isFavBaud(b) {
  return favBaudRates.value.includes(Number(b))
}

// 切换收藏当前波特率
function toggleFavBaud() {
  const b = Number(form.value.baudRate)
  const idx = favBaudRates.value.indexOf(b)
  if (idx >= 0) favBaudRates.value.splice(idx, 1)
  else favBaudRates.value.unshift(b)
}

// 应用主题（切换 html 根节点的 .dark 类）
function applyTheme() {
  const root = document.documentElement
  if (isDark.value) root.classList.add('dark')
  else root.classList.remove('dark')
}

function rpc(message) {
  return new Promise((resolve) => {
    const id = reqSeq++
    pending.set(id, resolve)
    ws.send(JSON.stringify({ ...message, id }))
    setTimeout(() => {
      if (pending.has(id)) {
        pending.delete(id)
        resolve({ ok: false, payload: 'timeout' })
      }
    }, 10000)
  })
}

function initBridge() {
  const candidates = resolveBridgeCandidates()
  let index = 0
  const notifyLocalBridgeMissing = () => {
    ElMessage.warning('未检测到本机串口网桥。请在本机安装 Node.js 并在 server 目录运行: npm install && npm run server')
  }
  const tryNext = () => {
    if (index >= candidates.length) {
      notifyLocalBridgeMissing()
      return
    }
    const url = candidates[index++]
    try { if (ws) ws.close() } catch (_) { ws = null }
    ws = new WebSocket(url)
    ws.onopen = () => {
      refreshPorts()
    }
    ws.onmessage = (evt) => {
      let msg
      try { msg = JSON.parse(String(evt.data)) } catch (_) { return }
      if (msg && typeof msg.id !== 'undefined' && pending.has(msg.id)) {
        const resolve = pending.get(msg.id)
        pending.delete(msg.id)
        resolve({ ok: !!msg.ok, payload: msg.payload })
        return
      }
      if (msg.type === 'status') onStatus(msg.payload || {})
      else if (msg.type === 'data') onData(String(msg.payload || ''))
      else if (msg.type === 'error') onError(String(msg.payload || ''))
    }
    ws.onclose = () => {
      connected.value = false
      tryNext()
    }
    ws.onerror = () => {
      // ignore; wait onclose to try next
    }
  }
  tryNext()
}

async function onModeChange() {
  try { localStorage.setItem('serialMode', mode.value) } catch (e) { void 0 }
  
  connected.value = false

  // Teardown previous mode and setup new mode
  if (preferBackendBridge.value) { // Switching to bridge
    if (serial) { // Teardown webserial
      try { await serial.close() } catch (_) { /* ignore */ }
      serial = null
    }
    initBridge()
  } else { // Switching to webserial
    if (ws) { // Teardown bridge
      ws.onclose = null // Prevent auto-reconnect
      try { ws.close() } catch (_) { /* ignore */ }
      ws = null
    }
    initWebSerial()
  }
  refreshPorts()
}

// 移除 Electron 专属的协议注册与原生检查逻辑
</script>

<style scoped>
.serial-panel { padding: 16px; }
.toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.status-tag { margin-left: 4px; }
.flex-spacer { flex: 1; }
.cards { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 12px; }
.card-title { font-weight: 600; letter-spacing: .5px; }
.send-panel { display: flex; flex-direction: column; }
.send-actions { display: flex; align-items: center; justify-content: space-between; margin-top: 8px; }
.send-history { display: flex; align-items: center; gap: 8px; }
.quick-cmds { margin-top: 8px; }
.quick-wrap { margin-top: 6px; display: flex; flex-wrap: wrap; gap: 6px; }
.quick-tag { cursor: pointer; }
.recv-toolbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.recv-box { height: 300px; overflow: auto; background: #0b0f14; color: #e6f1ff; padding: 10px; border-radius: 6px; border: 1px solid #1f2a37; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 12px; line-height: 1.6; }
.w-120 { width: 120px; }
.w-140 { width: 140px; }
.w-180 { width: 180px; }
.mt-8 { margin-top: 8px; }
@media (max-width: 980px) {
  .cards { grid-template-columns: 1fr; }
}
</style>


